iT邦幫忙

2023 iThome 鐵人賽

DAY 5
4
Software Development

CRUD仔的一生(上集)系列 第 6

[ACID] 樂觀鎖(OptimisticLock)

  • 分享至 

  • xImage
  •  

前情提要


前面我們介紹了 isolation level 的隱式鎖與顯示鎖,
這些都是確確實實的將資料加上鎖,
當資料被上鎖後,必須等待前者的鎖釋放後才能取用,
也就是俗稱的悲觀鎖。
有悲觀鎖,那麼有樂觀鎖嗎?
有多樂觀?樂觀能解決問題?
還真的能,而且可以做很多事情

樂觀鎖(Optimistic Lock)介紹

在介紹樂觀鎖之前,我們先看一下一般 table
以下是一個庫存表。

先看看悲觀的案例

我們先使用悲觀鎖(顯式鎖)的方式來更新數量,
當我們要更新庫存時,我們要先查詢目前數量是多少,並且讓其他 tx 不能讀不能改該筆庫存紀錄,
所以 select ... for update where id=1 來取出目前庫存,並且上鎖
然後使用update ... 來將 num-1,
最後在查詢一下目前更新後的數量,將更新結果回傳給使用者。

可能會這樣寫

begin
# for update 讓其他tx不可讀寫
select num from products where id=1 for update;
# 更新數量
update products set num=num-1 where id=1;
# 查詢最後剩餘多少
select num from products
commit

這麼樣其他 tx 在 select 時就會 block 住了(轉圈圈),
直到第一個 tx 釋放鎖後開始執行。

再來看看使用樂觀鎖的案例

實作樂觀鎖主要分成兩類:
Two widely known algorithms of optimistic concurrency control are:

我們這個範例,採取 version 方案。

我們需要改一下 table,加上 version 的欄位

樂觀鎖的整體關鍵就是在於 version 這個欄位,
查詢目前數量時,也帶上 version,告知接下來的 update 要更新 version 為 0 的那筆紀錄,
更新時,更新數量且更新 version,
下個 tx 如果還拿 version0 來更新時,
就不會更新到目前已經更新成 version1 的那筆資料了。

begin
select 'num','version' from products 
where id=1 and version=0;
update products set num=num-1, version=version+1 
where id=1 and version=0;
select num from products
commit

有沒有發現過程中完全沒上任何鎖
透過 version 來做 condition write。
我們看一下他是如何達成目標成果的。

分析

主要 tx 在 select 時,透過 version 的方式做控管,更新時也是依照 version 更新,
update 後,可以看到變動的 rows 數目,就可以得知這次的更改是否成功。
修改成功: select 時的 version 和 update 時的 version 相同,就可以成功更新,可以發現變動的 rows=1
修改失敗: select 時的 version 和 update 時的 version 不相同,更新不到,rows=0。
其他 tx 同理。

進化

我們可以透過變動的 rows=1 來確定是否修改成功,
但當發現 rows=0 時,我們可以透過 backoff retry 的方式在程式之中幫忙將 num-1 這個動作修改做到好
即發現 update rows=0 時,再重新 select version,update...,直到 update rows=1 為止。

優缺點

優點:

  1. 完全不需要上任何鎖,不會有等待鎖問題。
  2. 甚至連 read uncommit 的問題都沒有,因為做了 condition update

缺點:

  1. 在大併發的情況下,少了排隊等待鎖問題,更新資料變成比誰 retry 速度快了

樂觀鎖是好的方案嗎?

對於 db 演進的發展樂觀鎖是好的方案嗎?
要清楚這個問題,先拉回來我們原先認識的使用 SXlock db

完了丸了八比 Q 了
當我們在使用 讀鎖 (share lock) 時,會禁止其他 tx 做 update
而做作 寫鎖 (mutex lock) 時,會禁止其他 tx 做 select
對,你沒看錯 SXlock db,做 update 操作時,其他的 select 會被 block 住。
而且,當 commit 的時間點越後面,卡住的 tx 就會越多,更容易發生 deadlock 或 timeout 問題。

Timeout & rollack (deadlock detection),Wait-die (deadlock prevention)這兩大主題將放在下集做更詳細的介紹

但事情真的是這樣嗎?

為什麽 tx1 在 update 時,tx2 仍然可以讀取?
只有兩種可能

  1. 你的 db 在 update 時,沒上寫鎖 (mutex lock)
  2. 那是因為你可能正在使用 MVCC 的 db

MVCC (Multi-Version Concurrency Control)

類似剛剛的樂觀鎖,在資料表上加上 version 欄位,
每次的 insert/update 都會額外增加版本

MVCC 的資料庫的 Record 只有 X lock ,而沒有 S lock
MVCC 在讀取時,會最該 Record 「最新」的 Committed 版本(當前讀),所以自然地沒有了 Dirty Read
因為讀不會被阻擋,所以只有 write-write conflict

接下來將會介紹 Postgres 與 Mysql 實作的 MVCC

MVCC is type of Optimistic Lock? Yes!

  1. 悲觀鎖
    1. 隱式鎖
    2. 顯式鎖
  2. 樂觀鎖

參考資料

  1. Taipei 2019-04 course講義

上一篇
[ACID] 顯式鎖(The Locking Clause)
下一篇
[ACID] 樂觀鎖(MVCC In Postgres)
系列文
CRUD仔的一生(上集)32
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

1
雷N
iT邦研究生 1 級 ‧ 2023-09-20 00:18:31

每次的 insert/update 都會額外增加版本
這點講的真的很好, 所以MVCC上才需要一直清除過往歷史版本.
很適合讀取遠超過修改的比例時的場景, 提高讀取的吞吐量

悲觀鎖, 在修改比例特高時, 反而能透過互斥鎖降低衝突發生的機率

我要留言

立即登入留言